package org.shj.sso.util;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.springframework.util.Assert;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author Shen Huang Jian
 * @since 2018-07-18
 */
@Slf4j
public class HttpConnectionPoolUtil {

    /**
     * 设置连接建立的超时时间为10s
     */
    private static final int CONNECT_TIMEOUT = 10 * 1000;

    private static final int SOCKET_TIMEOUT = 10 * 1000;

    /**
     * 连接池的最大连接数
     */
    private static final int MAX_CONN = 100;

    /**
     * 连接到单个主机的最大连接数。
     */
    private static final int MAX_PRE_ROUTE = 50;
    private static final int MAX_ROUTE = 100;

    /**
     * 单位：毫秒。从连接池获取连接的等待时间。该值就是连接不够用的时候等待超时时间，一定要设置，而且不能太大
     */
    private static final int CONN_MANAGER_TIMEOUT = 500;

    /**
     * 相当于线程锁,用于线程安全
     */
    private final static Object SYNC_LOCK = new Object();

    /**
     * 连接池管理类
     */
    private static PoolingHttpClientConnectionManager manager;

    private static ScheduledExecutorService monitorExecutor;
    private static Map<String, CloseableHttpClient> hostClientMap = new ConcurrentHashMap<>();

    private static HttpRequestRetryHandler retryHandler;


    /**
     * @param url 以 http 或 https 开头的地址
     * @return
     */
    public static CloseableHttpClient getHttpClient(String url) {
        Assert.isTrue(StringUtils.isNotBlank(url), "参数 url 不能为空 ");
        Assert.isTrue(url.startsWith("http") || url.startsWith("https"), "参数 url 必须以 http 或 https 开头");

        String hostName = url.split("/")[2];

        int port = 80;
        if (hostName.contains(":")) {
            String[] args = hostName.split(":");
            hostName = args[0];
            port = Integer.parseInt(args[1]);
        }

        return createHttpClient(hostName, port);
    }

    /**
     * 根据host和port构建httpclient实例
     *
     * @param host 要访问的域名
     * @param port 要访问的端口
     * @return
     */
    private static CloseableHttpClient createHttpClient(String host, int port) {
        String key = host + ":" + port;
        if (hostClientMap.containsKey(key)) {
            return hostClientMap.get(key);
        }

        initPoolingConnectionManager();
        HttpHost httpHost = new HttpHost(host, port);
        //最后会把 HttpRoute 放在池中
        manager.setMaxPerRoute(new HttpRoute(httpHost), MAX_ROUTE);
        CloseableHttpClient client = HttpClients.custom().setConnectionManager(manager).setRetryHandler(retryHandler).build();
        hostClientMap.put(key, client);
        return client;
    }

    private static void initPoolingConnectionManager() {
        synchronized (SYNC_LOCK) {
            if (manager == null) {
                ConnectionSocketFactory plainSocketFactory = PlainConnectionSocketFactory.getSocketFactory();
                LayeredConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
                Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                        .register("http", plainSocketFactory)
                        .register("https", sslSocketFactory)
                        .build();
                //设置连接参数
                manager = new PoolingHttpClientConnectionManager(registry);
                // 最大连接数
                manager.setMaxTotal(MAX_CONN);
                // 路由最大连接数
                manager.setDefaultMaxPerRoute(MAX_PRE_ROUTE);

                retryHandler = new HttpRequestRetryHandler() {
                    @Override
                    public boolean retryRequest(IOException e, int i, HttpContext httpContext) {
                        if (i > 3) {
                            //重试超过3次,放弃请求
                            log.error("retry has more than 3 time, give up request");
                            return false;
                        }
                        if (e instanceof NoHttpResponseException) {
                            //服务器没有响应,可能是服务器断开了连接,应该重试
                            log.error("receive no response from server, retry");
                            return true;
                        }
                        if (e instanceof SSLHandshakeException) {
                            // SSL握手异常
                            log.error("SSL hand shake exception");
                            return false;
                        }
                        if (e instanceof InterruptedIOException) {
                            //超时
                            log.error("InterruptedIOException");
                            return false;
                        }
                        if (e instanceof UnknownHostException) {
                            // 服务器不可达
                            log.error("server host unknown");
                            return false;
                        }
                        if (e instanceof ConnectTimeoutException) {
                            // 连接超时
                            log.error("Connection Time out");
                            return false;
                        }
                        if (e instanceof SSLException) {
                            log.error("SSLException");
                            return false;
                        }
                        HttpClientContext context = HttpClientContext.adapt(httpContext);
                        HttpRequest request = context.getRequest();
                        if (!(request instanceof HttpEntityEnclosingRequest)) {
                            //如果请求不是关闭连接的请求
                            return true;
                        }
                        return false;
                    }
                };

                //开启监控线程,对异常和空闲线程进行关闭
                monitorExecutor = Executors.newScheduledThreadPool(1);
                monitorExecutor.scheduleAtFixedRate(new TimerTask() {
                    @Override
                    public void run() {
                        //关闭过期的连接
                        manager.closeExpiredConnections();

                        //关闭空闲的连接
                        manager.closeIdleConnections(10 * 1000, TimeUnit.MILLISECONDS);
                    }
                }, 20, 20, TimeUnit.SECONDS);
            }
        }
    }

    /**
     * 设置post请求的参数
     *
     * @param httpPost
     * @param params
     */
    private static void setPostParams(HttpPost httpPost, Map<String, String> params) {
        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        Set<String> keys = params.keySet();
        for (String key : keys) {
            nvps.add(new BasicNameValuePair(key, params.get(key)));
        }
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(nvps, "utf-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public static JSONObject post(String url, Map<String, String> params) {
        HttpPost httpPost = new HttpPost(url);
        setRequestConfig(httpPost);
        setPostParams(httpPost, params);
        CloseableHttpResponse response = null;
        JSONObject object = null;
        try {
            response = getHttpClient(url).execute(httpPost, HttpClientContext.create());
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                String bodyAsString = EntityUtils.toString(entity, "UTF-8");
                object = JSONObject.parseObject(bodyAsString);
            }

        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            try {
                if(response != null) {
                    response.close();
                }
            } catch (IOException e) {
                log.error("关闭连接时发生错误。", e);
            }
        }
        return object;
    }

    /**
     * 对http请求进行基本设置
     *
     * @param httpRequestBase http请求
     */
    public static void setRequestConfig(HttpRequestBase httpRequestBase) {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(CONN_MANAGER_TIMEOUT)
                .setConnectTimeout(CONNECT_TIMEOUT)
                .setSocketTimeout(SOCKET_TIMEOUT).build();
        httpRequestBase.setConfig(requestConfig);
    }

    /**
     * 关闭连接池
     */
    public static void closeConnectionPool() {
        try {
            for (String key : hostClientMap.keySet()) {
                hostClientMap.get(key).close();
            }
            manager.close();
            monitorExecutor.shutdown();
        } catch (IOException e) {
            log.error("关闭连接池时发生错误。", e);
        }
    }
}
